///////////////////////////////////////////////////////////////////////////////////////////////////
/// \file mpi_interface.h
///
/// \brief   provides an interface to mpi
///
///////////////////////////////////////////////////////////////////////////////////////////////////

#ifndef PCPS_MPI_INTERFACE_HEADER
#define PCPS_MPI_INTERFACE_HEADER

#include <src/pcps.h>
#include <src/archive.h>

#ifdef HAVE_MPI
#include <mpi.h>
#endif

#ifndef HAVE_MPI
namespace MPI {

  // declare some empty classes as placeholders if we are not using mpi
  class Comm {};
  class Op {};
  class Datatype {};
  extern MPI::Comm COMM_WORLD;
  extern MPI::Op SUM;
  extern MPI::Datatype CHAR;
  extern MPI::Datatype SHORT;
  extern MPI::Datatype INT;
  extern MPI::Datatype LONG;
  extern MPI::Datatype SIGNED_CHAR;
  extern MPI::Datatype UNSIGNED_CHAR;
  extern MPI::Datatype UNSIGNED_SHORT;
  extern MPI::Datatype UNSIGNED;
  extern MPI::Datatype UNSIGNED_LONG;
  extern MPI::Datatype FLOAT;
  extern MPI::Datatype DOUBLE;
  extern MPI::Datatype LONG_DOUBLE;
  extern MPI::Datatype BOOL;
  extern MPI::Datatype COMPLEX;
  extern MPI::Datatype DOUBLE_COMPLEX;
  extern MPI::Datatype LONG_DOUBLE_COMPLEX;

  inline double Wtime() { return double(std::clock()) / double(CLOCKS_PER_SEC); }

}
#endif

namespace pcps {

///////////////////////////////////////////////////////////////////////////////////////////////////
/// \brief   namespace to hold mpi related functions
///
///////////////////////////////////////////////////////////////////////////////////////////////////

namespace mpi {

  ///////////////////////////////////////////////////////////////////////////////////////////////////
  /// \brief   returns the global communicator
  ///
  /// \return the global communicator
  ///
  ///////////////////////////////////////////////////////////////////////////////////////////////////
  inline const MPI::Comm & world() {
    return MPI::COMM_WORLD;
  }

  ///////////////////////////////////////////////////////////////////////////////////////////////////
  /// \brief   returns the number of processes in a communicator
  ///
  /// \param[in]     comm     the communicator to use (defaults to MPI::COMM_WORLD)
  ///
  /// \return the number of processes in comm
  ///
  ///////////////////////////////////////////////////////////////////////////////////////////////////
  inline int size(const MPI::Comm & comm = MPI::COMM_WORLD) {
    #ifdef HAVE_MPI
    return comm.Get_size();
    #endif
    #ifndef HAVE_MPI
    return 1;
    #endif
  }

  ///////////////////////////////////////////////////////////////////////////////////////////////////
  /// \brief   returns rank of the current process in a communicator
  ///
  /// \param[in]     comm     the communicator to use (defaults to MPI::COMM_WORLD)
  ///
  /// \return the rank of the current process in comm
  ///
  ///////////////////////////////////////////////////////////////////////////////////////////////////
  inline int rank(const MPI::Comm & comm = MPI::COMM_WORLD) {
    #ifdef HAVE_MPI
    return comm.Get_rank();
    #endif
    #ifndef HAVE_MPI
    return 0;
    #endif
  }

  void init(int argc, char **argv);
  void finalize();
  std::string get_hostname();

  ///////////////////////////////////////////////////////////////////////////////////////////////////
  /// \brief   waits for all processes in the communicator
  ///
  /// \param[in]     comm     the communicator to use (defaults to MPI::COMM_WORLD)
  ///
  ///////////////////////////////////////////////////////////////////////////////////////////////////
  inline void barrier(const MPI::Comm & comm = MPI::COMM_WORLD) {
    #ifdef HAVE_MPI
    comm.Barrier();
    #endif
  }

  // declare mpi templates, setting the default communicator to the global communicator
  template<class T> void bcast(T * data, size_t n, int root = 0,
                               const MPI::Comm & comm = MPI::COMM_WORLD);
  template<class T> void reduce(const T * send_buf, T * recv_buf, size_t n, const MPI::Op & op, int root = 0,
                                const MPI::Comm & comm = MPI::COMM_WORLD);
  template<class T> void allreduce(const T * send_buf, T * recv_buf, size_t n, const MPI::Op & op,
                                   const MPI::Comm & comm = MPI::COMM_WORLD);
  template<class T> void send(const T * buf, size_t n, const int dest, const int tag,
                              const MPI::Comm & comm = MPI::COMM_WORLD);
  template<class T> void recv(T * buf, size_t n, const int source, const int tag,
                              const MPI::Comm & comm = MPI::COMM_WORLD);
  template<class T> void scatter(const T * send_buf, T * recv_buf, size_t n, int root = 0,
                                 const MPI::Comm & comm = MPI::COMM_WORLD);
  template<class T> void gather(const T * send_buf, T * recv_buf, size_t n, int root = 0,
                                const MPI::Comm & comm = MPI::COMM_WORLD);
  template<class T> void allgather(const T * send_buf, T * recv_buf, size_t n,
                                   const MPI::Comm & comm = MPI::COMM_WORLD);
  template<class T> void bcast(std::vector<T> & v, int root = 0, const MPI::Comm & comm = MPI::COMM_WORLD);
  template<class T> void reduce(const std::vector<T> & send_v, std::vector<T> & recv_v,
                                const MPI::Op & op, int root = 0, const MPI::Comm & comm = MPI::COMM_WORLD);
  template<class T> void allreduce(const std::vector<T> & send_v, std::vector<T> & recv_v,
                                   const MPI::Op & op, const MPI::Comm & comm = MPI::COMM_WORLD);
  template<class T> void send(const std::vector<T> & v, const int dest, const int tag,
                              const MPI::Comm & comm = MPI::COMM_WORLD);
  template<class T> void recv(std::vector<T> & v, const int source, const int tag,
                              const MPI::Comm & comm = MPI::COMM_WORLD);

  ///////////////////////////////////////////////////////////////////////////////////////////////////
  /// \brief   broadcast a single item
  ///
  /// \param[inout]  datum      the item to be broadcast
  /// \param[in]     root       the process from which to broadcast
  /// \param[in]     comm       the communicator to use
  ///
  ///////////////////////////////////////////////////////////////////////////////////////////////////
  template<class T> inline void bcast(T & datum, int root = 0, const MPI::Comm & comm = MPI::COMM_WORLD) {
    pcps::mpi::bcast(&datum, 1, root, comm);
  }

  ///////////////////////////////////////////////////////////////////////////////////////////////////
  /// \brief   broadcast a string
  ///
  /// \param[inout]  datum      the item to be broadcast
  /// \param[in]     root       the process from which to broadcast
  /// \param[in]     comm       the communicator to use
  ///
  ///////////////////////////////////////////////////////////////////////////////////////////////////
  inline void bcast(std::string & s, int root = 0, const MPI::Comm & comm = MPI::COMM_WORLD) {
    std::vector<char> v(s.begin(), s.end());
    pcps::mpi::bcast(v, root, comm);
    s = std::string(v.begin(), v.end());
  }

  ///////////////////////////////////////////////////////////////////////////////////////////////////
  /// \brief   broadcast a vector of strings
  ///
  /// \param[inout]  datum      the item to be broadcast
  /// \param[in]     root       the process from which to broadcast
  /// \param[in]     comm       the communicator to use
  ///
  ///////////////////////////////////////////////////////////////////////////////////////////////////
  inline void bcast(std::vector<std::string> & v, int root = 0, const MPI::Comm & comm = MPI::COMM_WORLD) {
    size_t n = v.size();
    pcps::mpi::bcast(n, root, comm);
    v.resize(n);
    for (size_t i = 0; i < n; i++)
      pcps::mpi::bcast(v[i], root, comm);
  }

  ///////////////////////////////////////////////////////////////////////////////////////////////////
  /// \brief   returns the appropriate MPI datatype
  ///
  /// \return the appropriate MPI datatype
  ///
  ///////////////////////////////////////////////////////////////////////////////////////////////////
  template<class T> inline const MPI::Datatype & datatype() {
    throw pcps::Exception("unknown mpi datatype");
    return MPI::DOUBLE;
  }
  template <> inline const MPI::Datatype & datatype< char                      >() { return MPI::CHAR;                }
  template <> inline const MPI::Datatype & datatype< signed short              >() { return MPI::SHORT;               }
  template <> inline const MPI::Datatype & datatype< signed int                >() { return MPI::INT;                 }
  template <> inline const MPI::Datatype & datatype< signed long               >() { return MPI::LONG;                }
  template <> inline const MPI::Datatype & datatype< signed char               >() { return MPI::SIGNED_CHAR;         }
  template <> inline const MPI::Datatype & datatype< unsigned char             >() { return MPI::UNSIGNED_CHAR;       }
  template <> inline const MPI::Datatype & datatype< unsigned short            >() { return MPI::UNSIGNED_SHORT;      }
  template <> inline const MPI::Datatype & datatype< unsigned int              >() { return MPI::UNSIGNED;            }
  template <> inline const MPI::Datatype & datatype< unsigned long int         >() { return MPI::UNSIGNED_LONG;       }
  template <> inline const MPI::Datatype & datatype< float                     >() { return MPI::FLOAT;               }
  template <> inline const MPI::Datatype & datatype< double                    >() { return MPI::DOUBLE;              }
  template <> inline const MPI::Datatype & datatype< long double               >() { return MPI::LONG_DOUBLE;         }
  template <> inline const MPI::Datatype & datatype< bool                      >() { return MPI::BOOL;                }
  template <> inline const MPI::Datatype & datatype< std::complex<float>       >() { return MPI::COMPLEX;             }
  template <> inline const MPI::Datatype & datatype< std::complex<double>      >() { return MPI::DOUBLE_COMPLEX;      }
  template <> inline const MPI::Datatype & datatype< std::complex<long double> >() { return MPI::LONG_DOUBLE_COMPLEX; }

  ///////////////////////////////////////////////////////////////////////////////////////////////////
  /// \brief   reads an object from an archive an broadcasts it
  ///
  /// \param[in,out] arch      the archive to read from
  /// \param[out]    val       the object that is read
  /// \param[in]     error_msg  an error message to display if the read fails
  /// \param[in]     root       which processor to broadcast from
  /// \param[in]     comm       the communicator to use (defaults to MPI::COMM_WORLD)
  ///
  ///////////////////////////////////////////////////////////////////////////////////////////////////
  template<class T> void read_and_bcast(pcps::Archive & arch,
                                        T & val,
                                        const std::string & error_msg = "failed to read from archive in read_and_bcast",
                                        int root = 0,
                                        const MPI::Comm & comm = MPI::COMM_WORLD) {

    // get MPI info
    const int nproc  = pcps::mpi::size(comm);
    const int myrank = pcps::mpi::rank(comm);

    // read the object on the root process
    if (myrank == root)
      if ( !(arch >> val) )
        throw pcps::Exception("%s") % error_msg;

    // broadcast the object
    pcps::mpi::bcast(val, root, comm);

  }

} // end namespace mpi

} // end namespace pcps

#endif
